Skip to content

feat(workspace-team): team workspaces, shared resources, workspace-scoped billing, and the #5517 redesign - #6142

Draft
lefarcen wants to merge 629 commits into
mainfrom
feat/workspace-team
Draft

feat(workspace-team): team workspaces, shared resources, workspace-scoped billing, and the #5517 redesign#6142
lefarcen wants to merge 629 commits into
mainfrom
feat/workspace-team

Conversation

@lefarcen

Copy link
Copy Markdown
Contributor

Why

Use case. feat/workspace-team is the long-running integration branch for the Workspace Team programme (OPEND-444): shared team workspaces, project/design-system/plugin sharing, workspace-scoped billing, the #5517 visual redesign, and the AMR/vela collaboration transport. It has been developed and dogfooded for weeks behind a build-time gate; this PR opens the trunk-merge conversation and gives us a single place to run full CI against main.

Pain being addressed. The branch is 497 commits ahead. Every extra day of divergence makes the merge more expensive (this PR already carries two main syncs and 42 hand-resolved conflicts), and trunk currently has no CI signal for any of this work. Opening the PR now buys us that signal and a shared reconciliation checklist.

This PR is not ready to merge. See "Blockers before merge" at the bottom. It is opened deliberately early so CI and reviewers can start; please do not enqueue it.

What users will see

Nothing changes for users on the stable channel when this merges. Team-workspace behaviour is gated at build time: apps/packaged/src/sidecars.ts only enables the collab/resource/team-projects transports and the vela web origin when amrProfile === "feature-test". Prod builds keep the current single-user experience until we flip that gate in a separate, deliberate change (sequenced after the vela backend ships — see the GA plan referenced below).

What is in the branch, and what users will see once the gate is flipped:

  • Team workspaces — create/switch workspaces, invite colleagues with a role, seat-aware invite flow.
  • Sharing — move a project into the team space; share design systems, plugins and skills; members get a read-only mirror with comments.
  • Workspace-scoped billing — balances, top-ups and run charges attributed to the active workspace instead of the personal account.
  • style(demo): UI polish round 3 — glass materials, browser add-to-chat, smoother tabs #5517 visual redesign — home hero, rail, tabs, template/plugin detail surfaces.
  • Collaboration transport — auto-pull of shared project content, presence avatars, inbound transfer progress.

Surface area

  • UI — extensive: workspace switcher, invite dialog, members/settings surfaces, sharing panels, style(demo): UI polish round 3 — glass materials, browser add-to-chat, smoother tabs #5517 redesign across home/rail/tabs/detail modals
  • Keyboard shortcut
  • CLI / env varod workspace/collab surfaces; OD_WORKSPACE_CONTEXT_SOURCE, OD_COLLAB_TRANSPORT, OD_RESOURCE_TRANSPORT, OD_TEAM_PROJECTS_TRANSPORT, OD_VELA_WEB_URL; OPEN_DESIGN_AMR_PROFILE=feature-test
  • API / contract/api/workspaces/*, /api/workspace/context, collab sync/pull/status/presence/comments endpoints, new SSE event types, new error codes (e.g. TEAM_PROJECT_OWNER_CONFLICT) in packages/contracts
  • Extension point
  • i18n keys — many, all 19 locales kept in sync via apps/web/src/i18n/types.ts
  • New top-level dependency — none added by this branch beyond the pinned @powerformer/vela-cli in tools/pack
  • Default behavior change — SQLite schema additions (workspace_projects, workspace-scoped resource tables); all behaviour behind the build-time profile gate for prod builds

Screenshots

Dogfood acceptance screenshots live in the Feishu tracker (base HTHrbGc4xaJwjss04SKc0ePSnFh, table tblTkanb8ddmWu9z), which is the authoritative status source for this programme; per-fix evidence is attached there record by record. Happy to inline a representative set here on request.

Bug fix verification

This is an integration PR, not a single bug fix. Every bug fix on the branch followed the repo's red-spec rule (red on pre-fix code, green on the fix) — recent examples:

  • f62271ae9 team members can comment on shared projects — red: apps/daemon/tests/project-comment-workspace-gate.test.ts (403 → 200)
  • 299802502 reconciliation judged by catalog membership — red: apps/daemon/tests/collab/workspace-projects-reconcile-membership.test.ts
  • bd249cc9a fresh-install wipe guard — red: apps/daemon/tests/collab-fresh-install-placeholder-guard.test.ts
  • 358145514 tab snapshots never adopted across identity scopes — red: apps/web/tests/components/WorkspaceTabsBar.test.tsx
  • 3aaa5dddc settled content replaces the frozen Comment-mode canvas — red: apps/web/tests/components/FileViewer.board-mode-content-update.test.tsx
  • cbcd8c4f0 template preview portal — red: apps/web/tests/components/template-modal-mapping.test.tsx

Validation

Run on the merge result of the two main syncs contained here:

  • pnpm guard — green
  • root pnpm typecheck (all workspaces incl. e2e) — green, 0 errors
  • pnpm --filter @open-design/daemon test — full suite; merge-introduced failures fixed, remaining 16 are pre-existing on the branch baseline (verified by re-running on a clean feat worktree)
  • pnpm --filter @open-design/web test — 5390 passing; 6 failures reproduce identically on the baseline
  • Conflict-surface batches re-run explicitly: FileViewer + edit-mode (363 cases), AvatarMenu/App/FileWorkspace/InlineModelSwitcher/SettingsDialog/InviteDialog/WorkspaceTabsBar (381 cases) — all green
  • Live dogfood acceptance across two real accounts (owner + member) on the feature-test backend for the collaboration paths; per-item evidence in the Feishu tracker

Blockers before merge

  1. Reconciliation of main UI work intentionally dropped during the syncs. Per an explicit product ruling ("the interface follows the feature branch — it has been through many rounds of acceptance"), these main changes were not carried into the branch and would be silently reverted by a naive merge:
  2. Release sequencing. Backend (vela) must ship first; the client gate flip is a separate change. See the GA rollout plan for the ordering and the feature-flag activation order.
  3. Playwright e2e/ui has not been run against the merged tree (typecheck/guard only). A ui_p0 run should gate the merge.
  4. Remaining P0 acceptance items tracked in the Feishu table (packaged transactional workspace binding matrix in particular).

lefarcen added 30 commits July 25, 2026 13:51
Both logout entry points — the nav-rail account menu's 退出登录 item and
the AMR account pill's Sign out button (Settings, chat error cards,
balance dialog) — used to clear the vela auth on a single click, so a
stray click signed the user out instantly.

They now arm a shared SignOutConfirmDialog built from the existing
@open-design/components Dialog primitives in the standard modal-confirm
alertdialog shape. Cancel/Escape leaves the session untouched; only the
dialog's confirm action runs the real logout chain (velaLogout + the
workspace-surface refresh nudges).

New signOut.confirm* i18n keys are defined in types.ts and translated in
all 19 locale files.
…ecvqholN2wcep)

The home 示例提示词 card for 交易分析仪表板模板 rendered a gray broken
frame instead of the template preview. Root cause: the daemon's sandboxed
preview route serves example.html with `default-src 'none'` CSP (no
frame-src), so a nested iframe can never load; the sanctioned pattern is
the iframe-only shell, which iframeOnlyHtmlShellTarget unwraps so the
inner template is served directly. This template's example.html wrapped
its iframe with a header strip, defeating the unwrap — the CSP-blocked
frame then baked into the CI gallery poster as a broken gray image.

- Rewrite example.html (both the plugins/_official copy and the
  design-templates mirror) as a bare iframe-only shell, matching the
  proven flowai-live-dashboard-template shape. The served preview now
  unwraps to the self-contained assets/template.html.
- Drop the stale broken bake entry from data/plugin-previews/manifest.json
  so clients fall back to the (now working) live HTML preview immediately,
  and the next CI bake sweep re-renders a fresh poster/clip.
- Add an e2e repo-consistency guard: any bundled example.html embedding a
  sibling .html through an iframe must be a pure shell (fails on the old
  wrapper, passes now), so this class of broken preview can't return.
…recvqg3CzK8GcT)

In the project chat composer footer the model provider mark sits between
the session-mode pill and the run/send capsule. Its box gaps were equal
(4px row gap + 4px trigger padding each side), but the visible spacing
was not: the pill's × glyph keeps ~17px of ink distance from the mark
(clear-button slack + pill trailing padding), while the solid send
capsule's edge sat only ~9px away — so the icon read as glued to the
run button (the two red boxes in the Feishu screenshot).

The trigger never paints its hitbox in this context (hover stays
transparent), so ink distance is the only spacing the eye can see. Add
margin-inline-end: 8px to the composer-row trigger so the right ink gap
matches the left one (17px on both sides). Scoped to .app .composer-row
and .chat-composer-fixed-layer .composer-row; the drawer tabs-bar and
home-hero switcher chips are untouched.

Verified by rendering the exact composer markup against the real
stylesheets (tokens/shell/chat/routines/plus-menu) in headless Chromium:
before = 17px/9px, after = 17px/17px, matching the pixel measurements
taken from the reporter's screenshot.
…n never-materialized team projects (recvqmnuxxKHaI)

A member daemon's team catalog routinely lists projects this member has
never opened or pulled. Those projects have no local `projects` row, and
`workspace_projects.project_id` is a FOREIGN KEY into `projects(id)`, so
the reconciler's bind fallback (`ensureWorkspaceProject` INSERT) threw
SQLITE_CONSTRAINT_FOREIGNKEY for every such project on every pass — hub
push and ~15s poller alike — accumulating 4700+ error lines across
restarts on the live member instance.

Reconciliation corrects the bindings of projects this daemon knows; it
must never materialize one. Materialization is owned by the open/pull
path (ensureSharedProjectPlaceholder / registerPulledProject), and the
team list already displays never-materialized projects straight from the
remote catalog. The reconciler now consults a new required
`hasLocalProject` dep and skips remote entries with neither a local
binding nor a local `projects` row — the same rebind-only stance its
request-scoped sibling `reconcileLocalRowWithRemoteTeamAccess` has
always taken. The demote direction is unaffected: demotes only ever come
from existing local bindings, which the FK guarantees are materialized.
… modal

A personal-workspace user with a $0 balance who clicked 「升级套餐」 on the
AMR balance gate landed on the TEAM dashboard's billing=checkout deep link,
which opens the Upgrade-Personal-workspace-to-Team dialog in an error state
("Team plan unavailable" / 3-seat minimum) — recvpYEiH019cD, reproduced live
with a real personal account during the failed acceptance round.

Root cause: every upgrade entry picked its branch on "does the context carry
a workspaceSettingsUrl", falling back to the personal wallet deep link only
when it was absent. B returns a settings URL for personal workspaces too
(mapVelaWorkspaceContext resolves it for every workspace type, and B's
console genuinely has a settings page for them), so the fallback never fired
and every workspace — personal included — took the team branch.

Fix: the axis is the workspace TYPE, not URL presence. A new shared
`workspaceUpgradeUrl` helper (EntryNavRail.tsx, next to `teamConsoleUrl`)
makes the one decision every upgrade affordance shares:

  - personal → `wallet?view=plans` (B's personal pricing modal; verified
    live to auto-open for the same third-account session, and vela's own
    wallet route gates the modal purely on `view=plans` for a personal
    workspace context)
  - team, never subscribed → `dashboard?billing=checkout` (unchanged from
    cb48e06)
  - team, already subscribed → `dashboard?billing=plan` (unchanged from
    cb48e06)

All five upgrade entries route through the helper instead of scattering the
condition: EntryNavRail's credits chip and seat-gate invite dialog,
AmrBalanceDialog's balance-gate CTA, RecentProjectsStrip's invite dialog,
and SettingsDialog's two AMR-card upgrade buttons (via its shared
amrUpgradeUrl closure). Dialog CTAs keep a profile-keyed fallback so they
still link somewhere when no console URL is known (signed out / context not
landed); list-surface entries hide the affordance instead.

teamConsoleUrl grows a 'plans' section (wallet + view=plans, keeping the
?workspaceId pin). For a TEAM workspace B redirects that exact URL into
dashboard?billing=checkout itself, so even a misrouted team session degrades
to the first-checkout dialog rather than a dead page.

Tests: the AmrBalanceDialog component suite grows the personal-workspace
case (red on the previous code: it produced /console/dashboard with
billing=checkout) alongside the existing never-subscribed / already-
subscribed team cases; team-console-url.test.ts covers workspaceUpgradeUrl's
three states plus the null/fallback contract; the EntryNavRail seat-gate
test's personal fixture now asserts the wallet pricing modal instead of the
team checkout link it used to encode.

Verified against the real feature-test runtime with the third acceptance
account (3101794733@qq.com, personal workspace, $0 balance): the balance
gate's 「升级套餐」 now opens
wallet?workspaceId=...&view=plans (no billing param), the URL family
confirmed live to auto-open B's personal pricing dialog.
…content-changed (recvqmKQRiIlYf)

When a teammate publishes a new version of a shared project, the hub
already pushes 'project-content-changed' over the collab SSE channel --
but the daemon only forwarded a thin nudge to an OPEN project view, so
freshness still hinged on the member web's ~5s /collab/status polling
and a closed tab never synced at all.

The daemon now owns discovery: on 'project-content-changed' it runs the
SAME pull flow POST /api/projects/:id/collab/pull runs (revocation gate,
owner-routed hub pull, register-on-pull, then the existing file-changed
and project-metadata-changed signals -- zero web changes needed), via a
handle registerCollabSyncRoutes returns. Guards, all fail-closed:

- only projects locally bound as team projects in workspace_projects
  are pulled; a share the member never materialized costs nothing;
- NEVER pulls a project this daemon's member owns -- the owner's local
  copy is the single writer (same rule as useProjectCollab's web-side
  auto-pull gate), and the owner daemon receives its own publish echo;
- event workspace, local binding workspace, and active identity must
  agree, and only an active team membership pulls;
- a per-project version cursor dedupes repeated/out-of-order events,
  and racing pulls (proactive vs the member web's poll-triggered POST)
  coalesce onto one in-flight materialization in collab-sync;
- failures degrade silently: the web polling fallback stays untouched.

Red-first specs: the three collab-sync handle tests fail on the branch
baseline (registerCollabSyncRoutes returned void); the guard matrix for
the new consumer lives in tests/collab/proactive-content-pull.test.ts.
Full daemon suite: 29 failures on this branch == the same 29 on the
clean baseline (langfuse-bridge/chat-route/etc., pre-existing).
lefarcen and others added 22 commits August 3, 2026 00:29
Harden Workspace collaboration authority, lifecycle cleanup, and browser coverage.\n\nRetry an expired authorized Team-project pull exactly once with a fresh receipt while preserving fail-closed scope, version, cleanup, and final freshness checks. Align the fake Vela receipt lifetime with the real two-second contract and use scalable E2E timeouts.
Record the current main ancestry while preserving the validated Workspace feature behavior and collaboration hardening.
Prevent stale Team-plugin listings, materializations, and delayed positive hub reads from reactivating a resource after retraction. Preserve independent Personal plugins with the same manifest ID and keep Team members able to share their own resources.

Add deterministic regression coverage for cache invalidation, binding lifecycle, and the superseded-positive activation race.
Reject invalid wallet balances without overwriting valid cache state, validate normalized proxy paths and Workspace scope, strip hop-by-hop headers, and tear down upstream requests on disconnect.

Keep team Workspace scope fail-closed when Connection nominates the protected header.
Preserve a verified explicit Workspace context when legacy directory rows omit the display-only workspaceName field. Keep exact-row name backfill when a non-empty name is present, without inferring authority from active or default Workspace state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps size/XXL PR changes 1500+ lines type/feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants